package dna;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;

import core.inputoutput.converters.FormatConverter;
import core.inputoutput.formatting.Format;
import core.molecules.atoms.Atom;
import core.molecules.atoms.Atom.SpecialState;
import core.molecules.organic.OrganicCompound;
import core.molecules.organic.amines.Amine;
import core.molecules.organic.phosphate.Phosphate;
import core.molecules.organic.sugars.Sugar;

import dna.deoxynucleosides.Deoxynucleoside;
import dna.macromolecules.DNAStrand;
import dna.macromolecules.DNAStructure;
import dna.nucleobase.Nucleobase;
import dna.nucleosides.Nucleoside;
import dna.sugars.Ribose;



import util.PDBParserException;
import util.XYZPosition;

/**
 * Format Converter designed to transform a DNA structure into a readable Coarse Grain PDB returning
 * a formatted PDB document with the essential Data determined by Johnny Maury's coarse grain model
 * simulator at NCSU
 * @author Julian
 *
 */
public class DNAtoCGconverter extends FormatConverter<DNAStructure> {

	//3-space constant
	private static final String S3 = "   ";

	public DNAtoCGconverter(Format f) {
		super(f);
	}

	@Override
	public String applyConverter(DNAStructure data) {

		notifyObservers("Reformatting...");
		StringBuilder sb = new StringBuilder();
		for (DNAStrand ds: data){
			notifyObservers("Strand " + data.indexOf(ds) + " of " + data.size()); //print update of status
			sb.append(convertToString(ds));
		}
		
		return sb.toString();
	}

	/**
	 * Converts the input strand to a set of string entries and corresponding connections
	 * @param strand
	 * @return
	 */
	private String convertToString(DNAStrand strand) {
		
		//deals with conflict between 3'/5' oxygens
		bindAllNucleosides(strand);
		
		StringBuffer atoms = new StringBuffer(), connects = new StringBuffer();
		int moleculeID = 1;
		for (Nucleoside dn : strand) { //iterate through nucleosides in the strand
			for (OrganicCompound comp: dn.getSubMolecules()){ //iterate through submolecules in the nucleoside (phosphate, nucleobase, sugar)
				String tag = null;
				
				//set coarse grain entry tag = color represented on PDB
				if (Nucleobase.class.isAssignableFrom(comp.getClass())) tag = findColorTag((Nucleobase)comp);
				else if (Phosphate.class.isAssignableFrom(comp.getClass())) tag = "P";
				else if (Ribose.class.isAssignableFrom(comp.getClass())) tag = "C";
				else throw new PDBParserException(PDBParserException.UNRECOGNIZED_DNA_SUBCOMP); 
				
				XYZPosition pos = calculateCenterOfMass(comp);
				atoms.append(createEntry(myFormat,
										   "ATOM" ,
										   Integer.toString(moleculeID++), 
										   tag, 
										   dn.getAbbr(), 
//										   Character.toString((char) (strand.getID()+64)),
										   Integer.toString(strand.getID()),
										   dn.getID().toString(),
										   trimDoubleToSize(Double.toString(pos.getX())), 
										   trimDoubleToSize(Double.toString(pos.Y)), 
										   trimDoubleToSize(Double.toString(pos.Z))));
			}
			connects.append(createConnects(moleculeID));
		}

		//Toggle this on and off depending on need. May be essential for CG documents to have "END" lines?
//		return atoms.append(connects.toString() + "END" + NL).toString();
		return atoms.append(connects.toString() + NL).toString();
	}



	
	/**
	 * In this case, binding all of the nucleotides together means ensuring that all O5' and O3' are held by the 
	 * appropriate phosphate rather than the suger
	 * @param data
	 */
	private void bindAllNucleosides(DNAStrand data) {
		Nucleoside fivePrime = null,
				   threePrime = null;
		for (Nucleoside cur : data){
			fivePrime = threePrime;
			threePrime = cur;
			if (fivePrime == null) continue;
			bindTogether(fivePrime, threePrime);
		}
	}

	/**
	 * binds the 3' end of the 5' molecule to the phosphate of the 3' molecule, and the phosphate in the 3' molecule to the sugar in the 3' molecule
	 * @param fivePrime
	 * @param threePrime
	 */
	//TODO: Improve to actually test bond distances rather than assume all cases are the same in a given molecule
	//Avg bond distance of PO4 < 4
	private void bindTogether(Nucleoside fivePrime, Nucleoside threePrime) {
		Atom a1 =null, a2 = null;
		if (threePrime.getPhosphate() == null) return;
		if (!fivePrime.getSugar().hasAtomsWithState(SpecialState.THREE_PRIME)){
			a1 = threePrime.getSugar().getAtomsWithState(SpecialState.THREE_PRIME).get(0);
			a2 = threePrime.getSugar().getAtomsWithState(SpecialState.FIVE_PRIME).get(0);
			threePrime.getSugar().removeAll(Arrays.asList(a1,a2));
		}
		else if (threePrime.getSugar().hasAtomsWithState(SpecialState.FIVE_PRIME)){
			a1 = fivePrime.getSugar().getAtomsWithState(SpecialState.THREE_PRIME).get(0);
			a2 = threePrime.getSugar().getAtomsWithState(SpecialState.FIVE_PRIME).get(0);
			fivePrime.getSugar().remove(a1);
			threePrime.getSugar().remove(a2);
		}
		threePrime.getPhosphate().addAll(Arrays.asList(a1,a2));
		
	}

	/**
	 * Formats the connects component of the CG output file, uses the conect Format enum
	 * @param i
	 * @return
	 */
	//TODO: this method could use some work
	private String createConnects(int i) {
		String connects = "";
		if (i > 3){
			if(i > 4)
				connects += createEntry(Format.CONECT, "CONECT", Integer.toString(i-4), Integer.toString(i-3));
			connects += createEntry(Format.CONECT, "CONECT", Integer.toString(i-3), Integer.toString(i-1));
		}
		connects += createEntry(Format.CONECT, "CONECT", Integer.toString(i-2), Integer.toString(i-1));
		return connects;
		
	}


	/**
	 * Calculates the center of mass of the given organic compound and returns it as an XYZ position
	 * @param c
	 * @return
	 */
	private XYZPosition calculateCenterOfMass(OrganicCompound c) {
		double x = 0, y = 0, z = 0;
		
		for(Atom a: c){
			x+=a.getPosition().getX()*a.getMass();
			y+=a.getPosition().getY()*a.getMass();
			z+=a.getPosition().getZ()*a.getMass();
		}
		x = x/c.getMass();
		y = y/c.getMass();
		z = z/c.getMass();
		
		return new XYZPosition(x, y, z);
	}

	/**
	 * Finds the PDB color tage associated with the given nucleotide
	 * @param nuc
	 * @return
	 */
	public String findColorTag(Nucleobase nuc) {
		switch (nuc.getAbbr().charAt(0)){
		case 'A': return "N";
		case 'C': return "S";
		case 'T': return "O";
		case 'G': return "Cl";
		case 'U': return null;
		}
		throw new PDBParserException(PDBParserException.UNRECOGNIZED_NUCLEOBASE);
		
	}


}
